﻿//(c) Alexandru Ghiondea, 2011 
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection;
using JSONDotNET.Syntax;

namespace JSONDotNET
{
    public class Deserializer
    {
        /// <summary>
        /// Deserializez a string containing a JSON object into an actual object
        /// </summary>
        /// <typeparam name="TObject">The type of the object that we want to deserialize from the JSON string</typeparam>
        /// <param name="text">The Compilation containing the result of a JSON parser</param>
        /// <returns>The deserialized object</returns>
        public static TObject Deserialize<TObject>(CompilationUnit cu)
        {
            return (TObject)deserializeObject(cu.Unit, (TObject)Activator.CreateInstance(typeof(TObject)));
        }

        /// <summary>
        /// Deserialize a string containing a JSON object into an actual object
        /// </summary>
        /// <typeparam name="TObject">The type of the object that we want to deserialize from the JSON string</typeparam>
        /// <param name="text">The text containing the JSON object</param>
        /// <returns>The deserialized object</returns>
        public static TObject Deserialize<TObject>(string text)
        {
            return Deserialize<TObject>(Parser.ParseText(text));
        }

        /// <summary>
        /// Finds a member with a given name from a list of MemberInfo 
        /// </summary>
        /// <param name="name">The name of the member to find</param>
        /// <param name="list">The list containing the members</param>
        /// <returns>The actual member, or null</returns>
        private static MemberInfo findMember(string name, IEnumerable<MemberInfo> list)
        {
            foreach (var item in list)
            {
                if (item.Name == name)
                    return item;
            }

            return null;
        }

        /// <summary>
        /// Deserialize an objects
        /// </summary>
        /// <param name="po">The parsed object</param>
        /// <param name="obj">The object to initialize</param>
        /// <returns>The initialized object</returns>
        private static object deserializeObject(ParsedObject po, object obj)
        {
            MemberInfo[] fields = obj.GetType().GetFields();
            MemberInfo[] props = obj.GetType().GetProperties();

            //we assign all the fields available
            foreach (var item in po.Members)
            {
                MemberInfo member = null;

                //we first try to find a member
                member = findMember(item.Name, fields);
                bool asProp = false;
                //then we try to find a property
                if (member == null)
                {
                    member = findMember(item.Name, props);
                    asProp = true;
                }

                Debug.Assert(member != null, "Could not find member " + item.Name + " on the type " + obj.GetType().Name);

                if (member == null)
                {
                    throw new MissingMemberException(string.Format("Could not find member {0} on type {1}", item.Name, obj.GetType().Name));
                }

                var typeOfMember = item.Value.GetType();

                object value = null;

                if (typeOfMember == typeof(ParsedObject))
                {
                    Type memberType = asProp ? (member as PropertyInfo).PropertyType : (member as FieldInfo).FieldType;

                    if (memberType == typeof(object))
                    {
                        // If the type of the member is object, then look for the HintTypeAttributes
                        // to see if we have some help with deserializing that object

                        foreach (var type in getTypeHints(member))
                        {
                            // Find the first type that is *NOT* a list or an array
                            // We do this because we know we are not deserializing into an array

                            // We found a ListType and we are trying to deserialize an array -- yay! :)
                            if (!(type.IsGenericType && type == typeof(List<>).MakeGenericType(type.GetGenericArguments())) && !type.IsArray)
                            {
                                memberType = type;
                                break;
                            }
                        }
                    }

                    value = deserializeObject(item.Value as ParsedObject, Activator.CreateInstance(memberType));
                }
                else if (typeOfMember == typeof(IntNumberValue))
                    value = (item.Value as IntNumberValue).Value;
                else if (typeOfMember == typeof(DoubleNumberValue))
                    value = (item.Value as DoubleNumberValue).Value;
                else if (typeOfMember == typeof(FalseValue))
                    value = FalseValue.Value;
                else if (typeOfMember == typeof(TrueValue))
                    value = TrueValue.Value;
                else if (typeOfMember == typeof(NullValue))
                    value = NullValue.Value;
                else if (typeOfMember == typeof(StringValue))
                    value = (item.Value as StringValue).Value;
                else if (typeOfMember == typeof(ArrayValue))
                // If the type of the field is object, we have to look for type hints{
                {
                    var memberType = asProp ? (member as PropertyInfo).PropertyType : (member as FieldInfo).FieldType;

                    if (memberType == typeof(object))
                    {
                        // we need to look at type hints!
                        foreach (var type in getTypeHints(member))
                        {
                            // We found a ListType and we are trying to deserialize an array -- yay! :)
                            if (type.IsGenericType && type == typeof(List<>).MakeGenericType(type.GetGenericArguments()))
                            {
                                // If we find a list, we are done! :)
                                memberType = type;
                                break;
                            }
                            else if (type.IsArray)
                            {
                                memberType = type;
                            }
                        }
                    }

                    value = deserializeArray(item.Value as ArrayValue,
                        /*The array is List<T>*/ memberType.IsArray ?
                                        Activator.CreateInstance(memberType, (item.Value as ArrayValue).Value.Count) :
                                        Activator.CreateInstance(memberType));
                }


                //depending on whether or not the member was a field or a property we call the respective property
                //if the type of field we assign to is an enum field...
                var temp = value;
                if (asProp)
                {
                    var prop = member as PropertyInfo;
                    temp = convertValueToType(value, prop.PropertyType);
                    prop.SetValue(obj, temp, null);
                }
                else
                {
                    var field = member as FieldInfo;
                    temp = convertValueToType(value, field.FieldType);
                    field.SetValue(obj, temp);
                }
            }
            return obj;
        }

        /// <summary>
        /// Deserializes an array of values. I will call into <see cref="deserializeObject"/> to deserialize the innert objects
        /// </summary>
        /// <param name="arrayValue"></param>
        /// <param name="obj"></param>
        /// <returns></returns>
        private static object deserializeArray(ArrayValue arrayValue, object obj)
        {
            MethodInfo method = null;
            bool asArray = false;
            // Figure out the element type for the array/list
            Type elementType = null;

            // Determine the method to call to add elements to a collection
            //is obj a List<[type]> ?
            if (obj.GetType().IsGenericType && obj.GetType() == typeof(List<>).MakeGenericType(obj.GetType().GetGenericArguments()))
            {
                // For lists we call the Add method on the object.
                // The element type of the list is the first (and only) generic argument
                method = obj.GetType().GetMethod("Add");

                var genericTypeArgs = obj.GetType().GetGenericArguments();
                Debug.Assert(genericTypeArgs.Length == 1, "A List should only have *ONE* type argument");
                elementType = genericTypeArgs[0];
            }
            else if (obj.GetType().IsArray)
            {
                // For arrays we need to call the SetValue on the underlying array type.
                method = obj.GetType().GetMethod("SetValue", new Type[] { typeof(object), typeof(int) });
                asArray = true;
                elementType = obj.GetType().GetElementType();
            }
            else
            {
                //we have no idea...
                throw new ArgumentException("the type of the file must be either a List<T> or an array", "obj");
            }

            int count = 0;
            foreach (var item in arrayValue.Value)
            {
                var typeOfMember = item.GetType();

                object value = null;

                if (typeOfMember == typeof(ParsedObject))
                    value = deserializeObject(item as ParsedObject, Activator.CreateInstance(asArray ? obj.GetType().UnderlyingSystemType.GetElementType() /*If this is an array*/ : obj.GetType().UnderlyingSystemType.GetGenericArguments()[0] /*If this is List<>*/));
                else if (typeOfMember == typeof(IntNumberValue))
                    value = (item as IntNumberValue).Value;
                else if (typeOfMember == typeof(DoubleNumberValue))
                    value = (item as DoubleNumberValue).Value;
                else if (typeOfMember == typeof(FalseValue))
                    value = FalseValue.Value;
                else if (typeOfMember == typeof(TrueValue))
                    value = TrueValue.Value;
                else if (typeOfMember == typeof(NullValue))
                    value = NullValue.Value;
                else if (typeOfMember == typeof(StringValue))
                    value = (item as StringValue).Value;
                else if (typeOfMember == typeof(ArrayValue))
                    value = deserializeArray(item as ArrayValue, Activator.CreateInstance(asArray ? obj.GetType().UnderlyingSystemType.GetElementType() /*If this is an array*/  : obj.GetType().UnderlyingSystemType.GetGenericArguments()[0] /*If this is List<>*/));

                // Convert the value to the elementtype
                var tempValue = convertValueToType(value, elementType);

                if (asArray)
                {
                    method.Invoke(obj, new object[] { tempValue, count++ });
                }
                else
                {
                    method.Invoke(obj, new object[] { tempValue });
                }
            }

            return obj;
        }

        /// <summary>
        /// Converts the value passed in to the type passed in.
        /// This will throw an exception if you want to convert a value to an incompatible type
        /// </summary>
        /// <param name="value">The value to convert</param>
        /// <param name="convertType">The type to which to convert the value</param>
        /// <returns></returns>
        private static object convertValueToType(object value, Type convertType)
        {
            var temp = value;
            if (convertType.IsEnum)
            {
                // if the value we are converting is not null
                // the value can be null for nullable enum t
                if (temp != null)
                    temp = Enum.Parse(convertType, temp.ToString(), true);
            }
            else if (convertType.IsGenericType && convertType.GetGenericTypeDefinition() == typeof(Nullable<>))
            {
                //This is a nullable type

                //We first convert the backing value to the right type
                var actualFieldType = convertType.GetGenericArguments()[0]; //there should only be 1 type argument!!!
                temp = convertValueToType(temp, actualFieldType);

                //And then we wrap it in a nullable type
                temp = temp.AsNullable(actualFieldType);
            }
            else if (temp != null && temp.GetType() != convertType)
            {
                // If we have a value and the type needs to be converted, convert it
                temp = temp.ChangeType(convertType);
            }

            return temp;
        }

        private static List<Type> getTypeHints(MemberInfo member)
        {
            List<Type> types = new List<Type>();

            foreach (TypeHintAttribute attr in member.GetCustomAttributes(typeof(TypeHintAttribute), false))
            {
                types.Add(attr.PotentialType);
            }
            return types;
        }
    }
}
